et2_dataview_controller.js ➔ _queueRequest   F
last analyzed

Complexity

Conditions 68

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 68
eloc 6
dl 0
loc 11
rs 0
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like et2_dataview_controller.js ➔ _queueRequest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * EGroupware eTemplate2
3
 *
4
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
5
 * @package etemplate
6
 * @subpackage dataview
7
 * @link http://www.egroupware.org
8
 * @author Andreas Stöckel
9
 * @copyright Stylite 2011-2012
10
 * @version $Id$
11
 */
12
13
/*egw:uses
14
	et2_core_common;
15
	et2_core_inheritance;
16
17
	et2_dataview_interfaces;
18
	et2_dataview_controller_selection;
19
	et2_dataview_view_row;
20
21
	egw_action.egw_action;
22
*/
23
24
/**
25
 * The fetch timeout specifies the time during which the controller tries to
26
 * consolidate requests for rows.
27
 */
28
var ET2_DATAVIEW_FETCH_TIMEOUT = 50;
29
30
var ET2_DATAVIEW_STEPSIZE = 50;
31
32
/**
33
 * The et2_dataview_controller class is the intermediate layer between a grid
34
 * instance and the corresponding data source. It manages updating the grid,
35
 * as well as inserting and deleting rows.
36
 */
37
var et2_dataview_controller = (function(){ "use strict"; return Class.extend({
38
39
	// Maximum concurrent data requests.  Additional ones are held in the queue.
40
	CONCURRENT_REQUESTS: 5,
41
42
	/**
43
	 * Constructor of the et2_dataview_controller, connects to the grid
44
	 * callback.
45
	 *
46
	 * @param _grid is the grid the controller should controll.
47
	 * @param _dataProvider is an object implementing the et2_IDataProvider
48
	 * interface.
49
	 * @param _rowCallback is the callback function that gets called when a row
50
	 * is requested.
51
	 * @param _linkCallback is the callback function that gets called for
52
	 * requesting action links for a row. The row data, the index of the row and
53
	 * the uid are passed as parameters to the function.
54
	 * uid is passed to the function.
55
	 * @param _context is the context in which the _rowCallback and the
56
	 * _linkCallback are called.
57
	 * @param _actionObjectManager is the object that manages the action
58
	 * objects.
59
	 */
60
	init: function (_parentController, _grid, _dataProvider, _rowCallback,
61
			_linkCallback, _context, _actionObjectManager)
62
	{
63
		// Copy the given arguments
64
		this._parentController = _parentController;
65
		this._grid = _grid;
66
		this._dataProvider = _dataProvider;
67
		this._rowCallback = _rowCallback;
68
		this._linkCallback = _linkCallback;
69
		this._context = _context;
70
71
		// Initialize list of child controllers
72
		this._children = [];
73
74
		// Initialize the "index map" which contains all currently displayed
75
		// containers hashed by the "index"
76
		this._indexMap = {};
77
78
		// Timer used for queing fetch requests
79
		this._queueTimer = null;
80
81
		// Array which contains all currently queued row indices in the form of
82
		// an associative array
83
		this._queue = {};
84
85
		// Current concurrent requests we have
86
		this._request_queue = [];
87
88
		// Register the dataFetch callback
89
		this._grid.setDataCallback(this._gridCallback, this);
90
91
		// Create the selection manager
92
		this._selectionMgr = new et2_dataview_selectionManager(
93
				this._parentController ? this._parentController._selectionMgr : null,
94
				this._indexMap,
95
				_actionObjectManager,
96
				this._selectionFetchRange,
97
				this._makeIndexVisible,
98
				this
99
		);
100
101
		// Record the child
102
		if(this._parentController != null)
103
		{
104
			this._parentController._children.push(this);
105
		}
106
	},
107
108
	destroy: function () {
109
110
		// Destroy the selection manager
111
		this._selectionMgr.free();
112
113
		// Clear the selection timeout
114
		this._clearTimer();
115
116
		// Remove the child from the child list
117
		if(this._parentController != null)
118
		{
119
			var idx = this._parentController._children.indexOf(this);
120
121
			if (idx >= 0)
122
			{
123
				// This element is no longer parent of the child
124
				this._parentController._children.splice(idx, 1);
125
				this._parentController = null;
126
			}
127
		}
128
	},
129
130
	/**
131
	 * The update function queries the server for changes in the currently
132
	 * managed index range -- those changes are then merged into the current
133
	 * view without a complete rebuild of every row.
134
	 *
135
	 * @param {boolean} clear Skip the fancy stuff, dump everything and start again.
136
	 *		Completely clears the grid and selection.
137
	 */
138
	update: function (clear) {
139
140
		// ---------
141
142
		// TODO: Actually stuff here should be done if the server responds that
143
		// there at all were some changes (needs implementation of "refresh")
144
145
		// Tell the grid not to try and update itself while we do this
146
		this._grid.doInvalidate = false;
147
148
		if(clear)
149
		{
150
			// Scroll to top
151
			this._grid.makeIndexVisible(0);
152
			this._grid.clear();
153
154
			// Free selection manager
155
			this._selectionMgr.clear();
156
157
			// Clear object manager
158
			this._objectManager.clear();
159
160
			// Clear the map
161
			this._indexMap = {}
162
			// Update selection manager, it uses this by reference
163
			this._selectionMgr.setIndexMap(this._indexMap);
164
165
			// Clear the queue
166
			this._queue = {};
167
168
			// Invalidate the change detection, re-fetches any known rows
169
			this._lastModification = 0;
170
		}
171
		// Remove all rows which are outside the view range
172
		this._grid.cleanup();
173
174
		// Get the currently visible range from the grid
175
		var range = this._grid.getIndexRange();
176
177
		// Force range.top and range.bottom to contain an integer
178
		if (range.top === false)
179
		{
180
			range.top = range.bottom = 0;
181
		}
182
		this._request_queue = [];
183
184
		// Require that range from the server
185
		this._queueFetch(et2_bounds(range.top, clear ? 0 : range.bottom + 1), 0, true);
186
	},
187
188
	/**
189
	 * Rebuilds the complete grid.
190
	 */
191
	reset: function () {
192
		// Throw away all internal mappings and reset the timestamp
193
		this._indexMap = {};
194
		// Update selection manager, it uses this by reference
195
		this._selectionMgr.setIndexMap(this._indexMap);
196
197
		// Clear the grid
198
		this._grid.clear();
199
200
		// Clear the row queue
201
		this._queue = {};
202
203
		// Reset the request queue
204
		this._request_queue = [];
205
206
		// Update the data
207
		this.update();
208
	},
209
210
	/**
211
	 * Loads the initial order. Do not call multiple times.
212
	 */
213
	loadInitialOrder: function (order) {
214
		for (var i = 0; i < order.length; i++)
215
		{
216
			this._getIndexEntry(i).uid = order[i];
217
		}
218
	},
219
220
	/**
221
	 * Load initial data
222
	 *
223
	 * @param {string} uid_key Name of the unique row identifier field
224
	 * @param {Object} data Key / Value mapping of initial data.
225
	 */
226
	loadInitialData: function (uid_prefix, uid_key, data) {
227
		var idx = 0;
228
		for(var key in data)
229
		{
230
			// Skip any extra keys
231
			if(typeof data[key] != "object" || data[key] == null || typeof data[key][uid_key] == "undefined") continue;
232
233
			// Add to row / uid map
234
			var entry = this._getIndexEntry(idx++);
235
			entry.uid = data[key][uid_key]+"";
236
			if(entry.uid.indexOf(uid_prefix) < 0)
237
			{
238
				entry.uid = uid_prefix + "::" + entry.uid;
239
			}
240
241
			// Add to data cache so grid will find it
242
			egw.dataStoreUID(entry.uid, data[key])
243
244
			// Don't try to insert the rows, grid will do that automatically
245
		}
246
		if(idx == 0)
247
		{
248
			// No rows, start with an empty
249
			this._selectionMgr.clear();
250
			this._emptyRow(this._grid._total == 0);
251
		}
252
	},
253
254
	/**
255
	 * Returns the depth of the controller instance.
256
	 */
257
	getDepth: function () {
258
259
		if (this._parentController)
260
		{
261
			return this._parentController.getDepth() + 1;
262
		}
263
264
		return 0;
265
	},
266
267
	/**
268
	 * Set the data cache prefix
269
	 * The default is to use appname, but if you need to set it explicitly to
270
	 * something else to avoid conflicts.  Use the same prefix everywhere for
271
	 * each type of data.  eg. infolog for infolog entries, even if accessed via addressbook
272
	 */
273
	setPrefix: function(prefix) {
274
		this.dataStorePrefix = prefix;
275
	},
276
277
	/**
278
	 * Returns the row information of the passed node, or null if not available
279
	 *
280
	 * @param {DOMNode} node
281
	 * @return {string|false} UID, or false if not found
282
	 */
283
	getRowByNode: function(node) {
284
		// Whatever the node, find a TR
285
		var row_node = jQuery(node).closest('tr');
286
		var row = false
287
288
		// Check index map - simple case
289
		var indexed = this._getIndexEntry(row_node.index());
290
		if(indexed && indexed.row && indexed.row.getDOMNode() == row_node[0])
291
		{
292
			row = indexed;
293
		}
294
		else
295
		{
296
			// Check whole index map
297
			for(var index in this._indexMap)
298
			{
299
				indexed = this._indexMap[index];
300
				if( indexed && indexed.row && indexed.row.getDOMNode() == row_node[0])
301
				{
302
					row = indexed;
303
					break;
304
				}
305
			}
306
		}
307
308
		// Check children
309
		for(var i = 0; !row && i < this._children.length; i++)
310
		{
311
			var child_row = this._children[i].getRowByNode(node);
312
			if(child_row !== false) row = child_row;
313
		}
314
		if(row && !row.controller)
315
		{
316
			row.controller = this;
317
		}
318
		return row;
319
	},
320
321
	/* -- PRIVATE FUNCTIONS -- */
322
323
324
	_getIndexEntry: function (_idx) {
325
		// Create an entry in the index map if it does not exist yet
326
		if (typeof this._indexMap[_idx] === "undefined")
327
		{
328
			this._indexMap[_idx] = {
329
				"row": null,
330
				"uid": null
331
			};
332
		}
333
334
		// Always update the index of the entries before returning them. This is
335
		// neccessary, as when we remove the uid from an entry without row, its
336
		// index does not get updated any further
337
		this._indexMap[_idx]["idx"] = _idx;
338
339
		return this._indexMap[_idx];
340
	},
341
342
	/**
343
	 * Inserts a new data row into the grid. index and uid are derived from the
344
	 * given management entry. If the data for the given uid does not exist yet,
345
	 * a "loading" placeholder will be shown instead. The function will do
346
	 * nothing if there already is a row associated to the entry. This function
347
	 * will not re-insert a row if the entry already had a row.
348
	 *
349
	 * @param _entry is the management entry for the index the row will be
350
	 * displayed at.
351
	 * @param _update specifies whether the row should be updated if _entry.row
352
	 * already exists.
353
	 * @return true, if all data for the row has been available, false
354
	 * otherwise.
355
	 */
356
	_insertDataRow: function (_entry, _update) {
357
		// Abort if the entry already has a row but the _insert flag is not set
358
		if (_entry.row && !_update)
359
		{
360
			return true;
361
		}
362
363
		// Context used for the callback functions
364
		var ctx = {"self": this, "entry": _entry};
365
366
		// Create a new row instance, if it does not exist yet
367
		var createdRow = false;
368
		if (!_entry.row)
369
		{
370
			createdRow = true;
371
			_entry.row = this._createRow(ctx);
372
			_entry.row.setDestroyCallback(this._destroyCallback, ctx);
373
		}
374
375
		// Load the row data if we have a uid for the entry
376
		this.hasData = false; // Gets updated by the _dataCallback
377
		if (_entry.uid)
378
		{
379
			// Register the callback / immediately load the data
380
			this._dataProvider.dataRegisterUID(_entry.uid, this._dataCallback,
381
					ctx);
382
		}
383
384
		// Display the loading "row prototype" if we don't have data for the row
385
		if (!this.hasData)
386
		{
387
			// Get the average height, the "-5" derives from the td padding
388
			var avg = Math.round(this._grid.getAverageHeight() - 5) + "px";
389
			var prototype = this._grid.getRowProvider().getPrototype("loading");
390
			jQuery("div", prototype).css("height", avg);
391
			var node = _entry.row.getJNode();
392
			node.empty();
393
			node.append(prototype.children());
394
		}
395
396
		// Insert the row into the table -- the same row must never be inserted
397
		// twice into the grid, so this function only executes the following
398
		// code only if it is a newly created row.
399
		if (createdRow && _entry.row)
400
		{
401
			this._grid.insertRow(_entry.idx, _entry.row);
402
		}
403
404
		return this.hasData;
405
	},
406
407
408
	/**
409
	 * Create a new row.
410
	 *
411
	 * @param {type} ctx
412
	 * @returns {et2_dataview_container}
413
	 */
414
	_createRow: function(ctx) {
415
		return new et2_dataview_row(this._grid);
416
	},
417
418
	/**
419
	 * Function which gets called by the grid when data is requested.
420
	 *
421
	 * @param _idxStart is the index of the first row for which data is
422
	 * requested.
423
	 * @param _idxEnd is the index of the last requested row.
424
	 */
425
	_gridCallback: function (_idxStart, _idxEnd) {
426
427
		var needsData = false;
428
429
		// Iterate over all elements the dataview requested and create a row
430
		// which indicates that we are currently loading data
431
		for (var i = _idxStart; i <= _idxEnd; i++)
432
		{
433
			var entry = this._getIndexEntry(i);
434
435
			// Insert the row for the entry -- do not update rows which are
436
			// already existing, as we do not have new data for those.
437
			if (!this._insertDataRow(entry, false) && needsData === false)
438
			{
439
				needsData = i;
440
			}
441
		}
442
443
		// Queue fetching that data range
444
		if (needsData !== false)
445
		{
446
			this._queueFetch(et2_bounds(needsData, _idxEnd + 1), needsData == _idxStart ? 0 : needsData > _idxStart ? 1 : -1, false);
447
		}
448
	},
449
450
	/**
451
	 * The _queueFetch function is used to queue a fetch request.
452
	 * TODO: Refresh is currently not used
453
	 */
454
	_queueFetch: function (_range, _direction, _isUpdate) {
455
456
		// Force immediate to be false
457
		_isUpdate = _isUpdate ? _isUpdate : false;
458
459
		// Push the requests onto the request queue
460
		var start = Math.max(0, _range.top);
461
		var end = Math.min(this._grid.getTotalCount(), _range.bottom);
462
		for (var i = start; i < end; i++)
463
		{
464
			if (typeof this._queue[i] === "undefined")
465
			{
466
				this._queue[i] = _direction; // Stage 1 - queue for after current, -1 -- queue for before current
467
			}
468
		}
469
470
		// Start the queue timer, if this has not already been done
471
		if (this._queueTimer === null && !_isUpdate)
472
		{
473
			var self = this;
474
			egw.debug('log', 'Dataview queue: ', _range);
475
			this._queueTimer = window.setTimeout(function () {
476
				self._flushQueue(false);
477
			}, ET2_DATAVIEW_FETCH_TIMEOUT);
478
		}
479
480
		if (_isUpdate)
481
		{
482
			this._flushQueue(true);
483
		}
484
	},
485
486
	/**
487
	 * Flushes the queue.
488
	 */
489
	_flushQueue: function (_isUpdate) {
490
491
		// Clear any still existing timer
492
		this._clearTimer();
493
494
		// Mark all elements in a radius of ET2_DATAVIEW_STEPSIZE
495
		var marked = {};
496
		var r = _isUpdate ? 0 : Math.floor(ET2_DATAVIEW_STEPSIZE / 2);
497
		var total = this._grid.getTotalCount();
498
		for (var key in this._queue)
499
		{
500
			if (this._queue[key] > 1)
501
				continue;
502
503
			key = parseInt(key);
504
505
			var b = Math.max(0, key - r + (r * this._queue[key]));
506
			var t = Math.min(key + r + (r * this._queue[key]), total - 1);
507
			var c = 0;
508
			for (var i = b; i <= t && c < ET2_DATAVIEW_STEPSIZE; i ++)
509
			{
510
				if (typeof this._queue[i] == "undefined"
511
						|| this._queue[i] <= 1)
512
				{
513
					this._queue[i] = 2; // Stage 2 -- pending or available
514
					marked[i] = true;
515
					c++;
516
				}
517
			}
518
		}
519
520
		// Create a list with start indices and counts
521
		var fetchList = [];
522
		var entry = null;
523
		var last = 0;
524
525
		// Get the int keys and sort the array numeric
526
		var arr = et2_arrayIntKeys(marked).sort(
527
				function(a,b){return a > b ? 1 : (a == b ? 0 : -1)});
528
529
		for (var i = 0; i < arr.length; i++)
530
		{
531
			if (i == 0 || arr[i] - last > 1)
532
			{
533
				if (entry)
534
				{
535
					fetchList.push(entry);
536
				}
537
				entry = {
538
					"start": arr[i],
539
					"count": 1
540
				};
541
			}
542
			else
543
			{
544
				entry.count++;
545
			}
546
547
			last = arr[i];
548
		}
549
550
		if (entry)
551
		{
552
			fetchList.push(entry);
553
		}
554
555
		// Special case: If there are no entries in the fetch list and this is
556
		// an update, create an dummy entry, so that we'll get the current count
557
		if (fetchList.length === 0 && _isUpdate)
558
		{
559
			fetchList.push({
560
				"start": 0, "count": 0
561
			});
562
563
			// Disable grid invalidate, or it might request again before we're done
564
			this._grid.doInvalidate = false;
565
		}
566
567
		egw.debug("log", "Dataview flush", fetchList);
568
		// Execute all queries
569
		for (var i = 0; i < fetchList.length; i++)
570
		{
571
			// Build the query
572
			var query = {
573
					"start": fetchList[i].start,
574
					"num_rows": fetchList[i].count,
575
					"refresh": false
576
			};
577
578
			// Context used in the callback function
579
			var ctx = {
580
					"self": this,
581
					"start": query.start,
582
					"count": query.num_rows,
583
					"lastModification": this._lastModification
584
			};
585
			if(this.dataStorePrefix)
586
			{
587
				ctx.prefix = this.dataStorePrefix;
588
			}
589
590
			this._queueRequest(query, ctx);
591
		}
592
	},
593
594
	/**
595
	 * Queue a request for data
596
	 * @param {Object} query
597
	 * @param {Object} ctx
598
	 */
599
	_queueRequest: function _queueRequest(query, ctx)
600
	{
601
		this._request_queue.push({
602
			query: query,
603
			context: ctx,
604
			// Start pending, set to 1 when request sent
605
			status: 0
606
		});
607
608
		this._fetchQueuedRequest();
609
	},
610
611
	/**
612
	 * Fetch data for a queued request, subject to rate limit
613
	 */
614
	_fetchQueuedRequest: function _fetchQueuedRequest()
615
	{
616
		// Check to see if there's room
617
		var count = 0;
618
		for (var i = 0; i < this._request_queue.length; i++)
619
		{
620
			if(this._request_queue[i].status > 0) count++;
621
		}
622
		// Too many requests, will try again after response is received
623
		if(count >= this.CONCURRENT_REQUESTS || this._request_queue.length === 0)
624
		{
625
			return;
626
		}
627
628
		// Keep at least 1 previous pending
629
		var keep = 1;
630
631
		// The most recent is the one the user's most interested in
632
		var request = null;
633
		for(var i = this._request_queue.length - 1; i >= 0; i--)
634
		{
635
			// Only interested in pending requests (status 0)
636
			if(this._request_queue[i].status != 0)
637
			{
638
				continue;
639
			}
640
			if(request == null)
641
			{
642
				request = this._request_queue[i];
643
			}
644
			else if (keep > 0)
645
			{
646
				keep--;
647
			}
648
			else if (keep <= 0)
649
			{
650
				// Cancel pending, they've probably scrolled past.
651
				this._request_queue.splice(i,1);
652
			}
653
		}
654
		if(request == null) return;
655
656
		// Request being sent
657
		request.status = 1;
658
659
		// Call the callback
660
		this._dataProvider.dataFetch(request.query, this._fetchCallback, request.context);
661
	},
662
663
	_clearTimer: function () {
664
665
		// Reset the queue timer upon destruction
666
		if (this._queueTimer)
667
		{
668
			window.clearTimeout(this._queueTimer);
669
			this._queueTimer = null;
670
		}
671
672
	},
673
674
	/**
675
	 * Called by the data source when the data changes
676
	 *
677
	 * @param _data Object|null New data, or null.  Null will remove the row.
678
	 */
679
	_dataCallback: function (_data) {
680
		// Set the "hasData" flag
681
		this.self.hasData = true;
682
683
		// Call the row callback with the new data -- the row callback then
684
		// generates the row DOM nodes that will be inserted into the grid
685
		if (this.self._rowCallback)
686
		{
687
			// Remove everything from the current row
688
			this.entry.row.clear();
689
690
			// If there's no data, stop
691
			if(typeof _data == "undefined" || _data == null)
692
			{
693
				this.self._destroyCallback.call(
694
					this,
695
					this.entry.row
696
				);
697
				return;
698
			}
699
700
			// Fill the row DOM Node with data
701
			this.self._rowCallback.call(
702
				this.self._context,
703
				_data,
704
				this.entry.row,
705
				this.entry.idx,
706
				this.entry
707
			);
708
709
			// Attach the "subgrid" tag to the row, if the depth of this
710
			// controller is larger than zero
711
			var tr = this.entry.row.getDOMNode();
712
			var d = this.self.getDepth();
713
			if (d > 0)
714
			{
715
				jQuery(tr).addClass("subentry");
716
				jQuery("td:first",tr).children("div").last().addClass("level_" + d + " indentation");
717
718
				if(this.entry.idx == 0)
719
				{
720
					// Set the CSS for the level - required so columns line up
721
					var indent = jQuery("<span class='indentation'/>").appendTo('body');
722
					egw.css(".subentry td div.innerContainer.level_"+d,
723
						"margin-right:" + (parseInt(indent.css("margin-right")) * d) + "px"
724
					);
725
					indent.remove();
726
				}
727
			}
728
729
			var links = null;
730
731
			// Look for a flag in the row to avoid actions.  Use for sums or extra header rows.
732
			if(!_data.no_actions)
733
			{
734
				// Get the action links if the links callback is set
735
				if (this.self._linkCallback)
736
				{
737
					links = this.self._linkCallback.call(
738
							this.self._context,
739
							_data,
740
							this.entry.idx,
741
							this.entry.uid
742
					);
743
				}
744
745
				// Register the row in the selection manager
746
				this.self._selectionMgr.registerRow(this.entry.uid, this.entry.idx,
747
						tr, links);
748
			}
749
			else
750
			{
751
				// Remember that
752
				this.entry.no_actions = true;
753
			}
754
755
			// Invalidate the current row entry
756
			this.entry.row.invalidate();
757
		}
758
	},
759
760
	/**
761
	 *
762
	 */
763
	_destroyCallback: function (_row) {
764
765
		// Unregister the row from the selection manager, if not selected
766
		// If it is selected, leave it there - allows selecting rows and scrolling
767
		var selection = this.self._selectionMgr._getRegisteredRowsEntry(this.entry.uid);
768
		if (this.entry.row && selection && !egwBitIsSet(selection.state, EGW_AO_STATE_SELECTED))
769
		{
770
			var tr = this.entry.row.getDOMNode();
771
			this.self._selectionMgr._updateState(this.entry.uid, EGW_AO_STATE_NORMAL)
772
			this.self._selectionMgr.unregisterRow(this.entry.uid, tr);
773
		}
774
775
		// There is no further row connected to the entry
776
		this.entry.row = null;
777
778
		// Unregister the data callback
779
		this.self._dataProvider.dataUnregisterUID(this.entry.uid,
780
				this.self._dataCallback, this);
781
	},
782
783
	/**
784
	 * Returns an array containing "_count" index mapping entries starting from
785
	 * the index given in "_start".
786
	 */
787
	_getIndexMapping: function (_start, _count) {
788
		var result = [];
789
790
		for (var i = _start; i < _start + _count; i++)
791
		{
792
			result.push(this._getIndexEntry(i));
793
		}
794
795
		return result;
796
	},
797
798
	/**
799
	 * Updates the grid according to the new order. The function simply does the
800
	 * following: It iterates along the new order (given in _order) and the old
801
	 * order given in _idxMap. Iteration variables used are
802
	 *     a) i -- points to the current entry in _order
803
	 *     b) idx -- points to the current grid row that will be effected by
804
	 *        this operation.
805
	 *     c) mapIdx -- points to the current entry in _indexMap
806
	 * The following cases may occur:
807
	 *     a) The current entry in the old order has no uid or no row -- in that
808
	 *        case the row at the current position is simply updated,
809
	 *        the old pointer will be incremented.
810
	 *     b) The two uids differ -- insert a new row with the new uid, do not
811
	 *        increment the old pointer.
812
	 *     c) The two uids are the same -- increment the old pointer.
813
	 * In a last step all rows that are left in the old order are deleted. All
814
	 * newly created index entries are returned. This function does not update
815
	 * the internal mapping in _idxMap.
816
	 */
817
	_updateOrder: function (_start, _count, _idxMap, _order) {
818
		// The result contains the newly created index map entries which have to
819
		// be merged with the result
820
		var result = [];
821
822
		// Iterate over the new order
823
		var mapIdx = 0;
824
		var idx = _start;
825
		for (var i = 0; i < _order.length; i++, idx++)
826
		{
827
			var current = _idxMap[mapIdx];
828
829
			if (!current.row || !current.uid)
830
			{
831
				// If there is no row yet at the current position or the uid
832
				// of that entry is unknown, simply update the entry.
833
				current.uid = _order[i];
834
				current.idx = idx;
835
836
				// Only update the row, if it is displayed (e.g. has a "loading"
837
				// row displayed) -- this is needed for prefetching
838
				if (current.row)
839
				{
840
					this._insertDataRow(current, true);
841
				}
842
843
				mapIdx++;
844
			}
845
			else if (current.uid !== _order[i])
846
			{
847
				// Insert a new row at the new position
848
				var entry = {
849
					"idx": idx,
850
					"uid": _order[i],
851
					"row": null
852
				};
853
854
				this._insertDataRow(entry, true);
855
856
				// Remember the new entry
857
				result.push(entry);
858
			}
859
			else
860
			{
861
				// Do nothing, the uids do not differ, just update the index of
862
				// the element
863
				current.idx = idx;
864
				mapIdx++;
865
			}
866
		}
867
868
		// Delete as many rows as we have left, invalidate the corresponding
869
		// index entry
870
		for (var i = mapIdx; i < _idxMap.length; i++)
871
		{
872
			if(typeof _idxMap[i] != 'undefined')
873
			{
874
				_idxMap[i].uid = null;
875
			}
876
		}
877
878
		return result;
879
	},
880
881
	_mergeResult: function (_newEntries, _invalidStartIdx, _diff, _total) {
882
883
		if (_newEntries.length > 0 || _diff > 0)
884
		{
885
			// Create a new index map
886
			var newMap = {};
887
888
			// Insert all new entries into the new index map
889
			for (var i = 0; i < _newEntries.length; i++)
890
			{
891
				newMap[_newEntries[i].idx] = _newEntries[i];
892
			}
893
894
			// Merge the old map with all old entries
895
			for (var key in this._indexMap)
896
			{
897
				// Get the corresponding index entry
898
				var entry = this._indexMap[key];
899
900
				// Calculate the new index -- if rows were deleted, we'll
901
				// have to adjust the index
902
				var newIdx = entry.idx >= _invalidStartIdx
903
						? entry.idx - _diff : entry.idx;
904
				if (newIdx >= 0 && newIdx < _total
905
				    && typeof newMap[newIdx] === "undefined")
906
				{
907
					entry.idx = newIdx;
908
					newMap[newIdx] = entry;
909
				}
910
				else
911
				{
912
					// Make sure the old entry gets invalidated
913
					entry.idx = null;
914
					entry.row = null;
915
				}
916
			}
917
918
			// Make the new index map the current index map
919
			this._indexMap = newMap;
920
			this._selectionMgr.setIndexMap(newMap);
921
		}
922
923
	},
924
925
	_fetchCallback: function (_response) {
926
		// Remove answered request from queue
927
		var request = null;
928
		for(var i = 0; i < this.self._request_queue.length; i++)
929
		{
930
			if(this.self._request_queue[i].context == this)
931
			{
932
				request = this.self._request_queue[i];
933
				this.self._request_queue.splice(i,1);
934
				break;
935
			}
936
		}
937
938
		this.self._lastModification = _response.lastModification;
939
940
		// Do nothing if _response.order evaluates to false
941
		if (!_response.order)
942
		{
943
			return;
944
		}
945
946
		// Make sure _response.order.length is not longer than the requested
947
		// count, if a specific count was requested
948
		var order = this.count != 0 ? _response.order.splice(0, this.count) : _response.order;
949
950
		// Remove from queue, or it will not be fetched again
951
		if(_response.total < this.count)
952
		{
953
			// Less rows than we expected
954
			// Clear the queue, or the remnants will never be loaded again
955
			this.self._queue = {};
956
		}
957
		else
958
		{
959
			for(var i = this.start; i < this.start + order.length; i++)
960
				delete this.self._queue[i];
961
		}
962
963
		// Get the current index map for the updated region
964
		var idxMap = this.self._getIndexMapping(this.start, order.length);
965
966
		// Update the grid using the new order. The _updateOrder function does
967
		// not update the internal mapping while inserting and deleting rows, as
968
		// this would move us to another asymptotic runtime level.
969
		var res = this.self._updateOrder(this.start, this.count, idxMap, order);
970
971
		// Merge the new indices, update all indices with rows that were not
972
		// affected and invalidate all indices if there were changes
973
		this.self._mergeResult(res, this.start + order.length,
974
				idxMap.length - order.length, _response.total);
975
976
		if(_response.total == 0)
977
		{
978
			this.self._emptyRow(true);
979
		}
980
		else
981
		{
982
			var row = jQuery(".egwGridView_empty",this.self._grid.innerTbody).remove();
983
			this.self._selectionMgr.unregisterRow("",0,row.get(0));
984
		}
985
986
		// Now it's OK to invalidate, if it wasn't before
987
		this.self._grid.doInvalidate = true;
988
989
		// Update the total element count in the grid
990
		this.self._grid.setTotalCount(_response.total);
991
		this.self._selectionMgr.setTotalCount(_response.total);
992
993
		// Schedule an invalidate, in case total is the same
994
		this.self._grid.invalidate();
995
996
		// Check if requests are waiting
997
		this.self._fetchQueuedRequest();
998
	},
999
1000
	/**
1001
	 * Insert an empty / placeholder row when there is no data to display
1002
	 */
1003
	_emptyRow: function(_noRows)
1004
	{
1005
		var noRows = !_noRows ? false : true;
1006
		jQuery(".egwGridView_empty",this._grid.innerTbody).remove();
1007
		if(typeof this._grid._rowProvider != "undefined" && this._grid._rowProvider.getPrototype("empty"))
1008
		{
1009
			var placeholder = this._grid._rowProvider.getPrototype("empty");
1010
			if(jQuery("td",placeholder).length == 1)
1011
			{
1012
				jQuery("td",placeholder).css("width",this._grid.outerCell.width() + "px")
1013
			}
1014
			placeholder.appendTo(this._grid.innerTbody);
1015
1016
			// Register placeholder action only if no rows
1017
			if (noRows)
1018
			{
1019
				// Get the action links if the links callback is set
1020
				var links = null;
1021
				if (this._linkCallback)
1022
				{
1023
					links = this._linkCallback.call(
1024
						this._context,
1025
						{},
1026
						0,
1027
						""
1028
					);
1029
				}
1030
				this._selectionMgr.registerRow("",0,placeholder.get(0), links);
1031
			}
1032
		}
1033
	},
1034
1035
	/**
1036
	 * Callback function used by the selection manager to translate the selected
1037
	 * range to uids.
1038
	 */
1039
	_selectionFetchRange: function (_range, _callback, _context) {
1040
		this._dataProvider.dataFetch(
1041
				{ "start": _range.top, "num_rows": _range.bottom - _range.top + 1,
1042
				  "no_data": true },
1043
				function (_response) {
1044
					_callback.call(_context, _response.order);
1045
				},
1046
				_context
1047
		);
1048
	},
1049
1050
	/**
1051
	 * Tells the grid to make the given index visible.
1052
	 */
1053
	_makeIndexVisible: function (_idx) {
1054
		this._grid.makeIndexVisible(_idx);
1055
	}
1056
1057
});}).call(this);
1058
1059